﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Windows.Threading;
using System.Threading;
using System.IO;
using ChromaCode.Extensions;

namespace ChromaCode
{
    /// <summary>
    /// Main implementation for the text highlighter control
    /// </summary>
    public class ChromaCode : RichTextBox
    {
        #region Public

        /// <summary>
        /// Constructor
        /// </summary>
        public ChromaCode()
        {
            m_HighlightTimer.Tick += new EventHandler(m_HighlightTimer_Tick);
            m_HighlightTimer.Interval = new TimeSpan(0, 0, 0, 0, 500);

            m_InternalUpdate = false;
            m_ParagraphDirty = false;
            m_RefreshParagraphOnTextChange = false;
            m_RefreshWholeDocOnTextChange = false;

            m_StartParagraph = CaretPosition.Paragraph;

            // Add handler for data being pasted into the control via any method and cut from the control
            DataObject.AddPastingHandler(this, ChromaCodePasteHandler);
            DataObject.AddCopyingHandler(this, ChromaCodeCopyHandler);
        }

        /// <summary>
        /// The document text in plain text format
        /// </summary>
        public string DocumentText
        {
            get
            {
                return (string)GetValue(DocumentTextProperty);
            }
            set
            {
                SetValue(DocumentTextProperty, value);
            }
        }

        /// <summary>
        /// Enable or disable the text highlighting in this control
        /// </summary>
        public bool EnableHighlighting
        {
            get
            {
                return (bool)GetValue(EnableHighlightingProperty);
            }
            set
            {
                SetValue(EnableHighlightingProperty, value);
            }
        }

        /// <summary>
        /// Load a rule set for the highlighter to use
        /// </summary>
        /// <param name="ruleXml">The xml file containing the rule set to use</param>
        public void LoadRules(string ruleXml)
        {
            m_Service.LoadRules(ruleXml);
        }

        /// <summary>
        /// Highlight a specific paragraph in the document
        /// </summary>
        /// <param name="paragraphToHighlight">The paragraph to do the highlighting on</param>
        public void DoHighlight(Paragraph paragraphToHighlight)
        {
            DoHighlight(paragraphToHighlight, false);
        }

        /// <summary>
        /// Apply highlighting to the whole document
        /// </summary>
        public void RefreshDocumentHighlighting()
        {
            Block documentBlock = Document.Blocks.FirstBlock;

            while (documentBlock != null)
            {
                // Loop through each block in the doucment and highlight any that are paragraphs
                if (documentBlock is Paragraph)
                {
                    Paragraph paragraphToHighlight = (Paragraph)documentBlock;
                    DoHighlight(paragraphToHighlight, true);
                }

                documentBlock = documentBlock.NextBlock;
            }
        }

        #endregion Public

        #region Protected

        /// <summary>
        /// When text is changed set the document text property to the plain text in the whole document
        /// </summary>
        /// <param name="e">Details about the event</param>
        protected override void OnTextChanged(TextChangedEventArgs e)
        {
            // We only want to do anything if this is an external update (i.e. the update has come from the user not
            // something within the control
            if (!m_InternalUpdate)
            {
                m_InternalUpdate = true;
                TextRange wholeDoc = new TextRange(Document.ContentStart, Document.ContentEnd);
                DocumentText = wholeDoc.Text;

                // Do we need to refresh the highlighting of the current paragraph on this change
                if (m_RefreshParagraphOnTextChange)
                {
                    DoHighlight(CaretPosition.Paragraph);
                    m_RefreshParagraphOnTextChange = false;
                }

                // Do we need to refresh the highlighting of the whole document on this change
                if (m_RefreshWholeDocOnTextChange)
                {
                    RefreshDocumentHighlighting();
                    m_RefreshWholeDocOnTextChange = false;
                }

                m_InternalUpdate = false;
            }

            base.OnTextChanged(e);
        }

        /// <summary>
        /// When ever a key is released on the key board a start timer to wait 0.5 second before recalculating any highlights.
        /// This is so that if the user keeps a key pressed then it will not cause pauses while highlighting is carried out
        /// until the key is released
        /// </summary>
        /// <param name="e">Details about the event</param>
        protected override void OnKeyUp(KeyEventArgs e)
        {
            // Start timer to wait 1 second before calculating what needs to be highlighted
            m_HighlightTimer.Start();

            base.OnKeyUp(e);
        }


        /// <summary>
        /// When a key is pressed stop the highlight timer. This effect of this and the starting of the timer in the KeyDown
        /// event means that highlighting will only take place if the user pauses in their typing for more than 0.5 seconds.
        /// </summary>
        /// <param name="e">Details about the event</param>
        protected override void OnPreviewKeyDown(KeyEventArgs e)
        {
            // Interrupt the highlight timer as we don't want to interrupt what the user is typing
            m_HighlightTimer.Stop();

            // Backspace and delete need to mark the paragraph as dirty
            if ((e.Key == Key.Back) || (e.Key == Key.Delete))
            {
                m_ParagraphDirty = true;
            }

            // If we are moving through the document and the current paragraph is dirty then update it
            if (IsMovementKey(e.Key) && (m_ParagraphDirty))
            {
                DoHighlight(CaretPosition.Paragraph);
            }

            m_StartParagraph = CaretPosition.Paragraph;

            // If the user pressed enter or the up or down arrows then force the highlight process on the current paragraph
            if (((e.Key == Key.Enter) || (e.Key == Key.Return)) && (m_ParagraphDirty))
            {
                DoHighlight(CaretPosition.Paragraph, false);
            }

            base.OnPreviewKeyDown(e);
        }

        /// <summary>
        /// This event is fired when actual text is input into the control
        /// </summary>
        /// <param name="e">Details about the event</param>
        protected override void OnTextInput(TextCompositionEventArgs e)
        {
            // Clear all highlighting at the Caret location if this is a printing character
            ClearCaretHighlight();

            // Mark the current paragraph as dirty to say that it has been changed
            m_ParagraphDirty = true;

            base.OnTextInput(e);
        }

        /// <summary>
        /// Whenever the mouse button is clicked then make sure we update the current paragraph that the cursor position
        /// is in
        /// </summary>
        /// <param name="e">Details about the event</param>
        protected override void OnMouseDown(MouseButtonEventArgs e)
        {
            m_StartParagraph = CaretPosition.Paragraph;

            base.OnMouseDown(e);
        }

        /// <summary>
        /// This event is fired just before the mouse button is clicked so this means that we could be moving to another
        /// paragraph. So refresh the highlighting on the paragraph we are currently in
        /// </summary>
        /// <param name="e">Details about the event</param>
        protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
        {
            if (m_ParagraphDirty)
            {
                DoHighlight(CaretPosition.Paragraph);
            }

            base.OnPreviewMouseDown(e);
        }

        
        #endregion Protected

        #region Private

        /// <summary>
        /// Clear highlighting at the Caret position so that typed text has no specific highlight. Makes it look a bit nicer
        /// when entering text
        /// </summary>
        private void ClearCaretHighlight()
        {
            TextPointer startPointer = CaretPosition.GetInsertationPointAtOffset(-1);
            TextPointer endPointer = CaretPosition.GetInsertationPointAtOffset(1);

            if ((startPointer != null) && (endPointer != null))
            {
                TextRange tr = new TextRange(startPointer, endPointer);
                tr.ClearAllProperties();
                tr.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Black);
            }
        }

        /// <summary>
        /// Highlight a specific paragraph in the document
        /// </summary>
        /// <param name="paragraphToHighlight">The paragraph to do the highlighting on</param>
        /// <param name="highlightWholeDoc">Thw whole document is having its highligting refreshed so some highlight
        /// processes need to be aware of this</param>
        private void DoHighlight(Paragraph paragraphToHighlight, bool highlightWholeDoc)
        {
            if (paragraphToHighlight != null)
            {
                if (EnableHighlighting)
                {
                    // We will only bother if enable highlighting is set to true
                    m_InternalUpdate = true;
                    BeginChange();
                    m_Service.HighlightRange(paragraphToHighlight, highlightWholeDoc);
                    EndChange();
                    m_InternalUpdate = false;
                }

                m_ParagraphDirty = false;
            }
        }

        /// <summary>
        /// Return true if the supplied key is a key that moves through the document (arrow, PgUp, Pg Down etc)
        /// </summary>
        /// <param name="key">The key we are testing</param>
        /// <returns>True if the key causes movement throughout the document</returns>
        private bool IsMovementKey(Key key)
        {
            bool result;

            switch (key)
            {
                case Key.Down:
                case Key.End:
                case Key.Home:
                case Key.PageDown:
                case Key.PageUp:
                case Key.Up:
                case Key.Left:
                case Key.Right:
                {
                    result = true;
                    break;
                }
                default:
                {
                    result = false;
                    break;
                }
            }

            return result;
        }

        /// <summary>
        /// Event fired when the timer fires
        /// </summary>
        /// <param name="sender">Who raised the event</param>
        /// <param name="e">Details about the event</param>
        private void m_HighlightTimer_Tick(object sender, EventArgs e)
        {
            DoHighlight(CaretPosition.Paragraph, false);

            m_HighlightTimer.Stop();
        }

        /// <summary>
        /// This is called when the DocumentText dependency property is changed
        /// </summary>
        /// <param name="sender">The object that contains the dependency object instance to change</param>
        /// <param name="e">Details about the event</param>
        private static void OnDocumentTextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ChromaCode thisControl = (ChromaCode)sender;

            if ((!thisControl.m_InternalUpdate) && (e.NewValue != null))
            {
                TextRange wholeDoc = new TextRange(thisControl.Document.ContentStart, thisControl.Document.ContentEnd);
                wholeDoc.Text = e.NewValue.ToString();
            }
        }

        /// <summary>
        /// This is called when the EnableHighlighting property is changed
        /// </summary>
        /// <param name="sender">The object that contains the dependency object instance to change</param>
        /// <param name="e">Details about the event</param>
        public static void OnEnableHighlightingChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            ChromaCode thisControl = (ChromaCode)sender;

            if (e.NewValue != null)
            {
                // Get the value that the user has set the EnableHighlighting property to
                bool enableHighlighting = false;
                if (e.NewValue is bool)
                {
                    enableHighlighting = (bool)e.NewValue;
                }

                if (enableHighlighting)
                {
                    // Re-apply highlighting to the whole control
                    thisControl.RefreshDocumentHighlighting();
                }
                else
                {
                    // Remove highlighting from all text
                    TextRange wholeDoc = new TextRange(thisControl.Document.ContentStart, thisControl.Document.ContentEnd);
                    wholeDoc.ClearAllProperties();
                }
            }
        }

        /// <summary>
        /// This method is set up to be called when ever data is pasted into the control. We need to make sure the 
        /// pasted text is highlighted correctly
        /// </summary>
        /// <param name="sender">Who raised the event</param>
        /// <param name="e">Details about the data pasted into the control</param>
        private void ChromaCodePasteHandler(object sender, DataObjectPastingEventArgs e)
        {
            m_RefreshWholeDocOnTextChange = true;
        }

        /// <summary>
        /// When data is copied/cut from the control then we need to refresh the current paragraph's highlighting
        /// </summary>
        /// <param name="sender">Who raised the event</param>
        /// <param name="e">Details about the data copied from the control</param>
        private void ChromaCodeCopyHandler(object sender, DataObjectCopyingEventArgs e)
        {
            m_RefreshParagraphOnTextChange = true;
        }

        /// <summary>
        /// Using a DependencyProperty as the backing store for DocumentText.  This enables animation, styling, binding, etc...
        /// </summary>
        private static readonly DependencyProperty DocumentTextProperty =
            DependencyProperty.Register("DocumentText", typeof(string), typeof(ChromaCode), new PropertyMetadata(OnDocumentTextChanged));

        /// <summary>
        /// Using a DependencyProperty as the backing store for EnableHighlighting.  This enables animation, styling, binding, etc...
        /// </summary>
        private static readonly DependencyProperty EnableHighlightingProperty =
            DependencyProperty.Register("EnableHighlighting", typeof(bool), typeof(ChromaCode), new PropertyMetadata(OnEnableHighlightingChanged));

        /// <summary>
        /// The document service used to manage the rules we will use and used to help apply the formatting
        /// </summary>
        private Services.DocumentService m_Service = new Services.DocumentService();

        /// <summary>
        /// The time that controls when we try to colour a line
        /// </summary>
        private DispatcherTimer m_HighlightTimer = new DispatcherTimer();

        /// <summary>
        /// When true the OnTextDocumentChanged() event is not processed
        /// </summary>
        private bool m_InternalUpdate;

        /// <summary>
        /// The paragraph that ther cursor started off in before the insertation process began. This is used so we know
        /// where to start highlighting from if we paste a block of text into the document
        /// </summary>
        private Paragraph m_StartParagraph;

        /// <summary>
        /// When the current paragraph has been altered then this will be true. The paragraph will only have its highlighting
        /// updated if this is true so it improves performance as we are not needlessly refreshing the highlighting
        /// </summary>
        private bool m_ParagraphDirty;

        /// <summary>
        /// If ths is true then when the OnTextChanged() event is fired the current paragraph will have its highlighting
        /// refreshed
        /// </summary>
        private bool m_RefreshParagraphOnTextChange;

        /// <summary>
        /// If this is true then when the OnTextChanged() event is fired the whole document will have its highlighting
        /// refreshed
        /// </summary>
        private bool m_RefreshWholeDocOnTextChange;

        #endregion Private
    }
}
